import torch
Extra-1: 딥러닝의 기초 (9)
벡터미분, 역전파와 기울기 소멸
강의영상
벡터미분: https://youtube.com/playlist?list=PLQqh36zP38-xYcyO28UluDhECTrTDDxCg
역전파와 기울기소멸: https://youtube.com/playlist?list=PLQqh36zP38-wQ4BAP4n5Nq9xWhHjUcL7J
이 강의는 2021년 빅데이터분석의 강의노트 및 강의영상을 편집하여 만들었습니다.
import
벡터미분
-
벡터미분에 대한 강의노트:
-
요약: 회귀분석에서 손실함수에 대한 미분은 아래와 같은 과정으로 계산할 수 있다.
\(loss = ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})={\bf y}^\top {\bf y} - {\bf y}^\top {\bf X}{\bf W} - {\bf W}^\top {\bf X}^\top {\bf y} + {\bf W}^\top {\bf X}^\top {\bf X} {\bf W}\)
\(\frac{\partial }{\partial {\bf W}}loss = -2{\bf X}^\top {\bf y} +2 {\bf X}^\top {\bf X} {\bf W}\)
체인룰
-
체인룰: 어려운 하나의 미분을 손쉬운 여러개의 미분으로 나누는 기법
-
손실함수가 사실 아래와 같은 변환을 거쳐서 계산되었다고 볼 수 있다.
- \({\bf X} \to {\bf X}{\bf W} \to {\bf y} -{\bf X}{\bf W} \to ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})\)
-
위의 과정을 수식으로 정리해보면 아래와 같다.
\({\bf u}={\bf X}{\bf W}\), \(\quad {\bf u}: n \times 1\)
\({\bf v} = {\bf y}- {\bf u},\) \(\quad {\bf v}: n \times 1\)
\(loss={\bf v}^\top {\bf v},\) \(\quad loss: 1 \times 1\)
-
손실함수에 대한 미분은 아래와 같다.
\[\frac{\partial }{\partial {\bf W}} loss = \frac{\partial }{\partial {\bf W}} {\bf v}^\top {\bf v}\]
(그런데 이걸 어떻게 계산함?)
-
계산할 수 있는것들의 모음..
\(\frac{\partial}{\partial {\bf v}} loss = 2{\bf v}\) \(\quad \to\) (n,1) 벡터
\(\frac{\partial }{\partial {\bf u}} {\bf v}^\top = -{\bf I}\) \(\quad \to\) (n,n) 매트릭스
\(\frac{\partial }{\partial \bf W}{\bf u}^\top = {\bf X}^\top\) \(\quad \to\) (p,n) 매트릭스
-
혹시.. 아래와 같이 쓸 수 있을까?
\[ \left(\frac{\partial }{\partial \bf W}{\bf u}^\top \right) \left(\frac{\partial }{\partial \bf u}{\bf v}^\top \right) \left(\frac{\partial }{\partial \bf v}loss \right) = \frac{\partial {\bf u}^\top}{\partial \bf W} \frac{\partial {\bf v}^\top}{\partial \bf u} \frac{\partial loss}{\partial \bf v} \]
- 가능할것 같다. 뭐 기호야 정의하기 나름이니까!
-
그렇다면 혹시 아래와 같이 쓸 수 있을까?
\[ \frac{\partial {\bf u}^\top}{\partial \bf W} \frac{\partial {\bf v}^\top}{\partial \bf u} \frac{\partial loss}{\partial \bf v} = \frac{\partial loss }{\partial\bf W}=\frac{\partial }{\partial \bf W} loss \]
- 이건 선을 넘는 것임.
- 그런데 어떠한 공식에 의해서 가능함. 그 공식 이름이 체인룰이다.
-
결국 정리하면 아래의 꼴이 되었다.
\[\left(\frac{\partial }{\partial \bf W}{\bf u}^\top \right) \left(\frac{\partial }{\partial \bf u}{\bf v}^\top \right) \left(\frac{\partial }{\partial \bf v}loss \right) = \frac{\partial }{\partial \bf W}loss \]
-
그렇다면?
\[\left({\bf X}^\top \right) \left(-{\bf I} \right) \left(2{\bf v}\right) = \frac{\partial }{\partial \bf W}loss \]
그런데, \({\bf v}={\bf y}-{\bf u}={\bf y} -{\bf X}{\bf W}\) 이므로
\[-2{\bf X}^\top\left({\bf y}-{\bf X}{\bf W}\right) = \frac{\partial }{\partial \bf W}loss \]
정리하면
\[\frac{\partial }{\partial \bf W}loss = -2{\bf X}^\top{\bf y}+2{\bf X}^\top {\bf X}{\bf W}\]
예시: 2021 빅데이터분석 중간고사 문제 2-(b)
-
미분계수를 계산하는 문제였음..
-
체인룰을 이용하여 미분계수를 계산하여 보자.
= torch.ones(5)
ones= torch.tensor([11.0,12.0,13.0,14.0,15.0])
x = torch.vstack([ones,x]).T
X = torch.tensor([17.7,18.5,21.2,23.6,24.2]) y
= torch.tensor([3.0,3.0]) W
= X@W
u = y-u
v = v.T @ v loss
loss
tensor(2212.1799)
-
$loss $ 의 계산
@ -torch.eye(5) @ (2*v) X.T
tensor([ 209.6000, 2748.5999])
-
참고로 중간고사 답은
@ -torch.eye(5)@ (2*v) / 5 X.T
tensor([ 41.9200, 549.7200])
입니다.
-
확인
= torch.tensor([3.0,3.0],requires_grad=True) _W
= (y-X@_W).T @ (y-X@_W) _loss
_loss.backward()
_W.grad.data
tensor([ 209.6000, 2748.5999])
-
\(\frac{\partial}{\partial \bf v} loss= 2{\bf v}\) 임을 확인하라.
v
tensor([-18.3000, -20.5000, -20.8000, -21.4000, -23.8000])
= torch.tensor([-18.3000, -20.5000, -20.8000, -21.4000, -23.8000],requires_grad=True) _v
= _v.T @ _v _loss
_loss.backward()
_v.grad.data, v
(tensor([-36.6000, -41.0000, -41.6000, -42.8000, -47.6000]),
tensor([-18.3000, -20.5000, -20.8000, -21.4000, -23.8000]))
-
\(\frac{\partial }{\partial {\bf u}}{\bf v}^\top\) 의 계산
= torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_u _u
tensor([36., 39., 42., 45., 48.], requires_grad=True)
= y - _u ### 이전의 _v와 또다른 임시 _v _v
(_v.T).backward()
RuntimeError: grad can be implicitly created only for scalar outputs
- 사실 토치에서는 스칼라아웃풋에 대해서만 미분을 계산할 수 있음
그런데 \(\frac{\partial}{\partial {\bf u}}{\bf v}^\top=\frac{\partial}{\partial {\bf u}}(v_1,v_2,v_3,v_4,v_5)=\big(\frac{\partial}{\partial {\bf u}}v_1,\frac{\partial}{\partial {\bf u}}v_2,\frac{\partial}{\partial {\bf u}}v_3,\frac{\partial}{\partial {\bf u}}v_4,\frac{\partial}{\partial {\bf u}}v_5\big)\) 이므로
조금 귀찮은 과정을 거친다면 아래와 같은 알고리즘으로 계산할 수 있다.
\(\frac{\partial }{\partial {\bf u}} {\bf v}^\top\)의 결과를 저장할 매트릭스를 만든다. 적당히
A
라고 만들자._u
하나를 임시로 만든다. 그리고 \(v_1\)을_u
로 미분하고 그 결과를A
의 첫번째 칼럼에 기록한다._u
를 또하나 임시로 만들고 \(v_2\)를_u
로 미분한뒤 그 결과를A
의 두번째 칼럼에 기록한다.(1)-(2)와 같은 작업을 \(v_5\)까지 반복한다.
(0)을 수행
= torch.zeros((5,5))
A A
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
(1)을 수행
u,v
(tensor([36., 39., 42., 45., 48.]),
tensor([-18.3000, -20.5000, -20.8000, -21.4000, -23.8000]))
= torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_u = (y-_u)[0] v1
- 이때 \(v_1=g(f({\bf u}))\)와 같이 표현할 수 있다. 여기에서 \(f((u_1,\dots,u_5)^\top)=(y_1-u_1,\dots,y_5-u_5)^\top\), 그리고 \(g((v_1,\dots,v_n)^\top)=v_1\) 라고 생각한다. 즉 \(f\)는 벡터 뺄셈을 수행하는 함수이고, \(g\)는 프로젝션 함수이다. 즉 \(f:\mathbb{R}^5 \to \mathbb{R}^5\)인 함수이고, \(g:\mathbb{R}^5 \to \mathbb{R}\)인 함수이다.
v1.backward()
_u.grad.data
tensor([-1., -0., -0., -0., -0.])
0]= _u.grad.data A[:,
A
tensor([[-1., 0., 0., 0., 0.],
[-0., 0., 0., 0., 0.],
[-0., 0., 0., 0., 0.],
[-0., 0., 0., 0., 0.],
[-0., 0., 0., 0., 0.]])
(2)를 수행
= torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_u = (y-_u)[1] v2
v2.backward()
_u.grad.data
tensor([-0., -1., -0., -0., -0.])
1]= _u.grad.data
A[:, A
tensor([[-1., -0., 0., 0., 0.],
[-0., -1., 0., 0., 0.],
[-0., -0., 0., 0., 0.],
[-0., -0., 0., 0., 0.],
[-0., -0., 0., 0., 0.]])
(3)을 수행 // 그냥 (1)~(2)도 새로 수행하자.
for i in range(5):
= torch.tensor([36., 39., 42., 45., 48.],requires_grad=True)
_u = (y-_u)[i]
_v
_v.backward()= _u.grad.data A[:,i]
A
tensor([[-1., -0., -0., -0., -0.],
[-0., -1., -0., -0., -0.],
[-0., -0., -1., -0., -0.],
[-0., -0., -0., -1., -0.],
[-0., -0., -0., -0., -1.]])
- 이론적인 결과인 \(-{\bf I}\)와 일치한다.
-
\(\frac{\partial }{\partial {\bf W}}{\bf u}^\top\)의 계산
\(\frac{\partial }{\partial {\bf W}}{\bf u}^\top = \frac{\partial }{\partial {\bf W}}(u_1,\dots,u_5)=\big(\frac{\partial }{\partial {\bf W}}u_1,\dots,\frac{\partial }{\partial {\bf W}}u_5 \big)\)
= torch.zeros((2,5))
B B
tensor([[0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0.]])
W
tensor([3., 3.])
= torch.tensor([3., 3.],requires_grad=True)
_W _W
tensor([3., 3.], requires_grad=True)
for i in range(5):
= torch.tensor([3., 3.],requires_grad=True)
_W = (X@_W)[i]
_u
_u.backward()= _W.grad.data B[:,i]
# X의 트랜스포즈 B
tensor([[ 1., 1., 1., 1., 1.],
[11., 12., 13., 14., 15.]])
X
tensor([[ 1., 11.],
[ 1., 12.],
[ 1., 13.],
[ 1., 14.],
[ 1., 15.]])
- 이론적인 결과와 일치
잠깐 생각해보자..
-
결국 위의 예제에 한정하여 임의의 \({\bf \hat{W}}\)에 대한 \(\frac{\partial}{\partial {\bf \hat W}}loss\)는 아래와 같이 계산할 수 있다.
- (단계1) \(2{\bf v}\)를 계산하고
- (단계2) (단계1)의 결과 앞에 \(-{\bf I}\)를 곱하고
- (단계3) (단계2)의 결과 앞에 \({\bf X}^\top\)를 곱한다.
-
step1에서 \({\bf v}\)는 어떻게 알지?
X \(\to\) u=X@W \(\to\) v = y-u
그런데 이것은 우리가 loss를 구하기 위해서 이미 계산해야 하는것 아니었나?
step1: yhat, step2: loss, step3: derivate, step4: update
-
(중요) step2에서 loss만 구해서 저장할 생각 하지말고 중간과정을 다 저장해라. (그중에 v와 같이 필요한것이 있을테니까) 그리고 그걸 적당한 방법을 통하여 이용하여 보자.
backprogation 알고리즘 모티브
-
아래와 같이 함수의 변환을 아키텍처로 이해하자. (함수의입력=레이어의입력, 함수의출력=레이어의출력)
- \({\bf X} \overset{l1}{\to} {\bf X}{\bf W} \overset{l2}{\to} {\bf y} -{\bf X}{\bf W} \overset{l3}{\to} ({\bf y}-{\bf X}{\bf W})^\top ({\bf y}-{\bf X}{\bf W})\)
-
그런데 위의 계산과정을 아래와 같이 요약할 수도 있다. (\({\bf X} \to {\bf \hat y} \to loss\)가 아니라 \({\bf W} \to loss({\bf W})\)로 생각해보세요)
- \({\bf W} \overset{l1}{\to} {\bf u} \overset{l2}{\to} {\bf v} \overset{l3}{\to} loss\)
-
그렇다면 아래와 같은 사실을 관찰할 수 있다.
- (단계1) \(2{\bf v}\)는 function of \({\bf v}\)이고, \({\bf v}\)는 l3의 입력 (혹은 l2의 출력)
- (단계2) \(-{\bf I}\)는 function of \({\bf u}\)이고, \({\bf u}\)는 l2의 입력 (혹은 l1의 출력)
- (단계3) 마찬가지의 논리로 \({\bf X}^\top\)는 function of \({\bf W}\)로 해석할 수 있다.
-
요약: \(2{\bf v},-{\bf I}, {\bf X}^\top\)와 같은 핵심적인 값들이 사실 각 층의 입/출력 값들의 함수꼴로 표현가능하다. \(\to\) 각 층의 입/출력 값들을 모두 기록하면 미분계산을 유리하게 할 수 있다.
- 문득의문: 각 층의 입출력값 \({\bf v}, {\bf u}, {\bf W}\)로 부터 \(2{\bf v}, -{\bf I}, {\bf X}^\top\) 를 만들어내는 방법을 모른다면 헛수고 아닌가?
- 의문해결: 어차피 우리가 쓰는 층은 선형+(렐루, 시그모이드, …) 정도가 전부임. 따라서 변환규칙은 미리 계산할 수 있음.
-
결국
(1)
순전파를 하면서 입출력값을 모두 저장하고
(2)
그에 대응하는 층별 미분계수값 \(2{\bf v}, -{\bf I}, {\bf X}^\top\) 를 구하고
(3)
층별미분계수값을 다시 곱하면 (그러니까 \({\bf X}^\top (-{\bf I}) 2{\bf v}\) 를 계산) 된다.
backpropagation
(1)
순전파를 계산하고 각 층별 입출력 값을 기록
- yhat = net(X)
- loss = loss_fn(yhat,y)
(2)
역전파를 수행하여 손실함수의 미분값을 계산
- loss.backward()
-
참고로 (1)에서 층별 입출력값은 GPU의 메모리에 기록된다.. 무려 GPU 메모리..
-
작동원리를 GPU의 관점에서 요약 (슬기로운 GPU 활용)
gpu특징: 큰 차원의 매트릭스 곱셈 전문가 (원리? 어마어마한 코어숫자)
- 아키텍처 설정: 모형의 파라메터값을 GPU 메모리에 올림 //
net.to("cuda:0")
- 순전파 계산: 중간 계산결과를 모두 GPU메모리에 저장 (순전파 계산을 위해서라면 굳이 GPU에 있을 필요는 없으나 후에 역전파를 계산하기 위한 대비) //
net(X)
- 오차 및 손실함수 계산:
loss = loss_fn(yhat,y)
- 역전파 계산: 순전파단계에서 저장된 계산결과를 활용하여 손실함수의 미분값을 계산 //
loss.backward()
- 다음 순전파 계산: 이전값은 삭제하고 새로운 중간계산결과를 GPU메모리에 올림
- 반복.
some comments
-
역전파기법은 체인룰 + \(\alpha\) 이다.
-
오차역전파기법이라는 용어를 쓰는 사람도 있다.
-
이미 훈련한 네트워크에 입력 \(X\)를 넣어 결과값만 확인하고 싶을 경우 순전파만 사용하면 되고, 이 상황에서는 좋은 GPU가 필요 없다.
기울기소멸
고요속의 외침
-
https://www.youtube.com/watch?v=ouitOnaDtFY
-
중간에 한명이라도 잘못 말한다면..
정의
-
In machine learning, the vanishing gradient problem is encountered when training artificial neural networks with gradient-based learning methods and backpropagation.
이해
-
당연한것 아닌가?
- 그레디언트 기반의 학습 (그레디언트 기반의 옵티마이저): 손실함수의 기울기를 통하여 업데이트 하는 방식
- 역전파: 손실함수의 기울기를 구하는 테크닉 (체인룰 + \(\alpha\)). 구체적으로는 (1) 손실함수를 여러단계로 쪼개고 (2) 각 단계의 미분값을 각각 구하고 (3) 그것들을 모두 곱하여 기울기를 계산한다.
- 0 근처의 숫자를 계속 곱하면 터지거나 0으로 간다. (사실 안정적인 기울기가 나올 것이라고 생각하는것 자체가 이상함)
import numpy as np
= np.random.uniform(low=-2,high=2,size=100)
grads grads
array([-0.04807346, -1.29968172, 0.60383275, -1.11097504, -1.44903838,
-1.81770819, -0.82995838, -0.30307517, -1.62486386, 1.14841271,
0.05317044, -1.63595745, -1.27254039, -0.2793212 , 1.4193291 ,
-1.87957006, -1.50413435, -1.6143583 , -1.97979251, -0.1319077 ,
-0.36277507, 0.24188449, 0.8205258 , -1.15353317, -0.94630341,
0.60335535, -1.4326661 , 1.27171997, 1.51390194, -0.3285052 ,
-1.56389259, -1.55964141, 0.29636461, -0.74477874, -1.6119124 ,
0.01097634, -0.25255016, 1.80684873, 0.90766818, -0.25062987,
-0.74179267, -0.50494702, 1.89992546, -0.2911778 , 0.3116119 ,
1.63679034, 0.86030588, -1.10851323, -1.13171181, -1.58949804,
0.82233945, -1.81269013, -0.31257892, 1.00176613, -0.49897359,
-1.05885742, 0.49664799, -1.02531506, -0.81051658, -1.34188376,
-1.98882591, 0.32634551, 1.39557143, -1.13150272, -1.5616825 ,
-0.0478864 , -1.34446087, -0.56475749, -1.90177931, -0.38516623,
0.75889489, -1.86710065, -1.85428312, 1.82248239, 1.04363342,
1.08128997, -0.36466711, -1.99204485, 0.81840524, -1.11993576,
-0.53666968, -0.09132125, 1.57303259, 1.82113986, 0.31948837,
0.34639304, -1.68151764, 1.0845407 , -0.01560537, 0.73393873,
0.85033511, 1.98350062, -0.45107395, 1.45704639, 1.48581033,
-1.57196761, 0.92182647, 0.78667051, -1.01047196, -0.13306075])
grads.prod()
-3.5832605040779705e-14
- 기울기가 소멸함
= np.random.uniform(low=-5,high=5,size=100)
grads grads.prod()
2.2118230498890502e+26
- 기울기가 폭발함.
= np.random.uniform(low=-1,high=3.5,size=100)
grads grads.prod()
5.058085058858178e-07
-
도깨비: 기울기가 소멸하기도 하고 터지기도 한다.
해결책 (기울기 소멸에 대한 해결책)
-
Multi-level hierarchy
- 여러층을 쪼개서 학습하자 \(\to\) 어떻게? 사전학습, 층벼학습
- 기울기소실문제를 해결하여 딥러닝을 유행시킨 태초의(?) 방법임.
- 결국 입력자료를 바꾼뒤에 학습하는 형태
-
Gradient clipping
- 너무 큰 값의 기울기는 사용하지 말자. (기울기 폭발에 대한 대비책)
-
Faster hardware
- GPU를 중심으로 한 테크닉
- 근본적인 문제해결책은 아니라는 힌튼의 비판
- CPU를 쓸때보다 GPU를 쓰면 약간 더 깊은 모형을 학습할 수 있다 정도?
-
Residual Networks, LSTM
- 아키텍처를 변경하는 방법
-
Other activation functions
- 렐루의 개발
-
배치정규화
- 어쩌다보니 되는것.
- 배치정규화는 원래 공변량 쉬프트를 잡기 위한 방법임. 그런데 기울기 소멸에도 효과가 있음. 현재는 기울기소멸문제에 대한 해결책으로 빠짐없이 언급되고 있음. 2015년의 원래 논문에는 기울기소멸에 대한 언급은 없었음. (https://arxiv.org/pdf/1502.03167.pdf)
- 심지어 배치정규화는 오버피팅을 잡는효과도 있음 (이것은 논문에 언급했음)
-
기울기를 안구하면 안되나?
- 베이지안 최적화기법: (https://arxiv.org/pdf/1807.02811.pdf) \(\to\) GPU를 어떻게 쓰지? \(\to\) 느리다